/*
 * Licensed to the 南京汇海交通科技有限公司
 *
 * Copyright (c) 2025.
 * All rights reserved.
 *
 * 本软件属南京汇海交通科技有限公司所有，在未获得南京汇海交通科技有限公司正式授权情况下，任何企业和个人，不能获取、阅读、安装、传播本软件涉及的任何受知识产权保护的内容。
 */
package com.cje.gateway.security.handler;

import com.cje.gateway.security.config.ClientDetails;
import com.cje.toolkit.FluxSessionUserUtils;
import com.chframework.boot.core.response.status.ApiStatusEnum;
import com.chframework.common.exception.ApplicationException;
import com.chframework.common.exception.SystemInterrupterException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Primary;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.List;

/**
 * 认证处理
 * 根据用户数据，确认令牌是否有效
 */
@Slf4j
@Primary
@Component
@RequiredArgsConstructor
public class SessionAuthenticationManager implements ReactiveAuthenticationManager {

    @Override
    public Mono<Authentication> authenticate(Authentication authentication) {
        Object authenticationDetails = authentication.getDetails();
        if (!(authenticationDetails instanceof ClientDetails clientDetails)) {
            return Mono.error(new SystemInterrupterException(ApiStatusEnum.ERR_A0320));
        }
        // 获取请求头中的token【客户端】
        String accessToken = (String) authentication.getCredentials();
        // 获取当前会话对象映射的登录名【服务器端，在会话管理过滤器中解析WebSession对象得到】
        String loginName = (String) authentication.getPrincipal();

        /*
         * 会话管理模块整体设计思路：
         *（1）一个IP可以登录多个账号，因此，将会话对象与终端用户空间绑定；
         *（2）一个账号可以登录多个IP，因此，将会话对象与终端IP绑定；
         *（3）防止Access-Token被窃取或租借，需保证该参数与终端用户空间和终端IP两个参数都成功匹配时，认证流程才能放行！
         */
        return Mono.just(clientDetails)
                .doOnNext(authDetails -> {
                    // 此处的校验需要考虑一种特殊情况：
                    // 当前gateway实例通过WebClient调用当前gateway实例管理的web-api时会第二次执行此处的`访问源`验证，此时则需要直接放行
                    // 特殊场景：{@link com.taiping.dlt.client.service.AuthorizeServiceClient.createWebClient(ServerWebExchange)}
                })
                .then(FluxSessionUserUtils.getCurrentVisitor())
                .flatMap(authLoginBO -> {
                    log.info("[{}]token `{}` 对应的缓存用户为 {}",
                            clientDetails.getClientRequestId(),
                            accessToken,
                            authLoginBO.getUsername());
                    /*
                     * 此处特别说明，在登录通认证成功之后，
                     * 对象AuthLoginBO包括：
                     * （1）登录认证信息；
                     * （2）用户详细信息；
                     * （3）用户角色信息；
                     * （4）用户权限数据；
                     * （5）用户在终端显示层的菜单数据
                     *
                     * 唯一需要注意的一点：
                     * 前端在获取当前登录用户数据时，用户的部分隐私数据需要进行脱敏处理，如账号密码、手机号、邮箱等
                     *
                     * 企业统一认证中心项目还未进行开发，大致功能模块包括：① 统一登录管理；② 统一用户管理；③ 统一权限管理；
                     */
                    // 此处，暂时以硬代码的方式模拟从缓存中取出了本地登录用户的权限数据。

                    // 用户角色信息，一个用户可以同时拥有多个角色。
                    List<SimpleGrantedAuthority> roleCodeList = authLoginBO.getUser()
                            .getRoles()
                            .stream()
                            .map(t -> new SimpleGrantedAuthority(String.valueOf(t.getRoleId())))
                            .toList();

                    // 三个参数：登录名，允许访问的API列表，当前登录用户的角色列表
                    UsernamePasswordAuthenticationToken newAuthentication =
                            new UsernamePasswordAuthenticationToken(loginName, accessToken, roleCodeList);
                    newAuthentication.setDetails(clientDetails);

                    return Mono.just(newAuthentication);
                })
                .cast(Authentication.class)
                .onErrorResume(e -> Mono.error(new ApplicationException(e)));
    }

}
